package com.bac.rentmap.ad.flat.transform;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.logging.Logger;

import javax.jdo.PersistenceManager;
import javax.jdo.Query;

import org.springframework.beans.factory.annotation.Required;
import org.springframework.util.comparator.ComparableComparator;
import org.springframework.util.comparator.CompoundComparator;
import org.springframework.util.comparator.NullSafeComparator;

import com.bac.rentmap.ad.flat.FlatAd;
import com.bac.rentmap.ad.flat.FlatAdDao;
import com.bac.rentmap.ad.flat.crawler.FlatCrawledAd;
import com.bac.rentmap.ad.flat.image.FlatImageService;
import com.bac.rentmap.ad.flat.jdo.JdoFlatAdDao;
import com.bac.rentmap.ad.flat.jdo.JdoHelper;
import com.bac.rentmap.ad.transform.CrawledAdTransformer;
import com.bac.rentmap.ad.transform.Result;
import com.bac.rentmap.ad.util.DateHelper;
import com.bac.rentmap.jdo.JdoSupport;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;

/**
 * It is not guarantied that for each {@link FlatCrawledAd} new {@link FlatAd}
 * will be created. In case of failure some {@link FlatCrawledAd}s can be lost.
 * Additionally there is possibility that some {@link FlatImage} become
 * orphaned. This is OK, they will be cleared in some time by old flat image
 * processor.
 * 
 * @author Andrew Osipenko
 */
public class FlatCrawledAdTransformer extends JdoSupport implements
        CrawledAdTransformer<FlatCrawledAd> {
    private static final Logger log = Logger.getLogger(JdoFlatAdDao.class
            .getName());

    private FlatAdDao flatAdDao;
    private int batchSize;
    private FlatImageService flatImageService;
    private DateHelper dateHelper;
    private JdoHelper jdoHelper;
    private PhoneBlackListService phoneBlackListService;

    @SuppressWarnings({ "unchecked", "rawtypes" })
    public Result transform() {
        PersistenceManager persistenceManager = getPersistenceManager();
        try {
            Query query = buildQuery(persistenceManager);
            query.setRange(0, batchSize);
            // TODO: uncomment
            // query.setOrdering("id desc");
            List<FlatCrawledAd> flatCrawledAds = (List<FlatCrawledAd>) query
                    .execute(Boolean.TRUE);
            log.info("Transforming " + flatCrawledAds.size()
                    + " FlatCrawledAds");
            final Comparator comparator = new NullSafeComparator(
                    new ComparableComparator(), true);
            Map<FlatAd, FlatCrawledAd> flatMapping = new HashMap<FlatAd, FlatCrawledAd>();
            Collection<FlatAd> flatAds = new TreeSet<FlatAd>(
                    new CompoundComparator(new Comparator[] {
                            new Comparator<FlatAd>() {

                                public int compare(FlatAd o1, FlatAd o2) {
                                    return comparator.compare(o1.getStreet(),
                                            o2.getStreet());
                                }
                            }, new Comparator<FlatAd>() {

                                public int compare(FlatAd o1, FlatAd o2) {
                                    return comparator.compare(o1.getBuilding(),
                                            o2.getBuilding());
                                }
                            }, new Comparator<FlatAd>() {

                                public int compare(FlatAd o1, FlatAd o2) {
                                    return comparator.compare(o1.getPhone(),
                                            o2.getPhone());
                                }
                            }

                    }));
            for (FlatCrawledAd flatCrawledAd : flatCrawledAds) {
                log.fine("Proccessing " + flatCrawledAd);
                if(phoneBlackListService.isInBlackList(flatCrawledAd))
                    continue;
                FlatAd flatAd = flatAdDao.find(flatCrawledAd.getStreet(),
                        flatCrawledAd.getBuilding(), flatCrawledAd.getPhone(),
                        persistenceManager);
                if (flatAd != null) {
                    log.fine(flatAd
                            + " crawled allready with last publishing date "
                            + flatAd.getLastPublishingDate());
                    if (flatCrawledAd.getPublishingDate() != null
                            && flatCrawledAd.getPublishingDate().after(
                                    flatAd.getLastPublishingDate())) {
                        log.info("Updating "
                                + flatCrawledAd.getPublishingDate());
                        flatAd.setLastPublishingDate(flatCrawledAd
                                .getPublishingDate());
                    }
                } else {
                    flatAd = transform(flatCrawledAd);
                    flatAds.add(flatAd);
                    flatMapping.put(flatAd, flatCrawledAd);
                }
                flatAd.setRating(null);
            }
            saveFlatAds(new ArrayList(flatAds), flatMapping, persistenceManager);
            persistenceManager.deletePersistentAll(flatCrawledAds);
            return new Result(flatCrawledAds.size(), flatAds.size());
        } finally {
            persistenceManager.close();
        }
    }

    private void saveFlatAds(List<FlatAd> flatAds,
            Map<FlatAd, FlatCrawledAd> flatMapping,
            PersistenceManager persistenceManager) {
        List<Entity> flatAdEntities = convert2entities(flatAds, flatMapping,
                persistenceManager);
        if (!flatAdEntities.isEmpty()) {
            saveEntities(flatAdEntities, flatAds, flatMapping);
        }
    }

    private List<Entity> convert2entities(List<FlatAd> flatAds,
            Map<FlatAd, FlatCrawledAd> flatMapping,
            PersistenceManager persistenceManager) {
        List<Entity> flatAdEntities = new ArrayList<Entity>();
        for (FlatAd flatAd : flatAds) {
            FlatCrawledAd flatCrawledAd = flatMapping.get(flatAd);
            Key flatCrawledAdKey = jdoHelper
                    .convertToFlatCrawledAdKey(flatCrawledAd.getId());
            List<Long> flatImageIds = flatImageService.getFlatImageIds(
                    flatCrawledAdKey, persistenceManager);
            flatAd.setFlatImages(convert2flatImageKeys(flatImageIds));
            log.fine("Converting for saving " + flatAd);
            flatAdEntities.add(flatAd.convert2entity());
        }
        return flatAdEntities;
    }

    private List<Key> convert2flatImageKeys(List<Long> flatImageIds) {
        List<Key> res = new ArrayList<Key>(flatImageIds.size());
        for (Long flatImageId : flatImageIds) {
            res.add(jdoHelper.convertToFlatImageKey(flatImageId));
        }
        return res;
    }

    private void saveEntities(List<Entity> flatAdEntities,
            List<FlatAd> flatAds, Map<FlatAd, FlatCrawledAd> flatMapping) {
        DatastoreService datastoreService = DatastoreServiceFactory
                .getDatastoreService();
        List<Key> keys = datastoreService.put(flatAdEntities);
        for (int i = 0; i < flatAds.size(); i++) {
            FlatAd flatAd = flatAds.get(i);
            FlatCrawledAd flatCrawledAd = flatMapping.get(flatAd);
            flatImageService.attachToFlatAd(flatCrawledAd.getId(), keys.get(i)
                    .getId(), flatAd.getLastPublishingDate());
        }
    }

    private FlatAd transform(FlatCrawledAd flatCrawledAd) {
        FlatAd res = new FlatAd();
        res.setLocalAddress(flatCrawledAd.getLocalAddress());
        res.setCity(flatCrawledAd.getCity());
        res.setPrice(flatCrawledAd.getPrice());
        if(flatCrawledAd.getPricePerDay() != null) {
            res.setPricePerDay(flatCrawledAd.getPricePerDay());
        }
        res.setPhone(flatCrawledAd.getPhone());
        String description = flatCrawledAd.getDescription();
        if (description != null)
            res.setDescription(description);
        Date publishingDate = flatCrawledAd.getPublishingDate();
        if (publishingDate == null) {
            publishingDate = dateHelper.getDateForMidnight();
        }
        res.setFirstPublishingDate(publishingDate);
        res.setLastPublishingDate(publishingDate);
        res.setRoom(flatCrawledAd.getRoom());
        res.setSourceUrl(flatCrawledAd.getSourceUrl());
        res.setExternalUrl(flatCrawledAd.getExternalUrl());
        res.setExternalImages(flatCrawledAd.getExternalImages());
        res.setSecureId(flatCrawledAd.getSecureId());
        res.setEmail(flatCrawledAd.getEmail());
        return res;
    }

    @Required
    public void setFlatAdDao(FlatAdDao flatAdDao) {
        this.flatAdDao = flatAdDao;
    }

    @Required
    public void setBatchSize(int batchSize) {
        this.batchSize = batchSize;
    }

    public int getQueueSize() {
        PersistenceManager persistenceManager = getPersistenceManager();
        try {
            Query query = buildQuery(persistenceManager);
            query.setResult("count(this)");
            Object res = query.execute(Boolean.TRUE);
            if(res == null)
                return 0;
            return ((Number) res).intValue();
        } finally {
            persistenceManager.close();
        }
    }

    private Query buildQuery(PersistenceManager persistenceManager) {
        Query query = persistenceManager.newQuery(FlatCrawledAd.class);
        query.setFilter("readyForTransformation == readyForTransformationParam");
        query.declareParameters("Boolean readyForTransformationParam");
        return query;
    }

    public int getBatchSize() {
        return batchSize;
    }

    @Required
    public void setFlatImageService(FlatImageService flatImageService) {
        this.flatImageService = flatImageService;
    }

    @Required
    public void setDateHelper(DateHelper dateHelper) {
        this.dateHelper = dateHelper;
    }

    @Required
    public void setJdoHelper(JdoHelper jdoHelper) {
        this.jdoHelper = jdoHelper;
    }

    @Required
    public void setPhoneBlackListService(PhoneBlackListService phoneBlackListService) {
        this.phoneBlackListService = phoneBlackListService;
    }
}